package org.restlesscode.javersion.transcoders.bean;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.restlesscode.javersion.SvnObject;
import org.restlesscode.javersion.SvnPropertyObject;
import org.restlesscode.javersion.Transcoder;
import org.restlesscode.javersion.transcoders.bean.annotations.SvnContent;
import org.restlesscode.javersion.transcoders.bean.annotations.SvnProperty;
import org.restlesscode.javersion.transcoders.bean.annotations.SvnStorable;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Transcoder for storing a POJO using annotations to specify which fields to store.
 * @param <T> Class type to transcoder.
 */
public class AnnotatedBeanTranscoder<T> implements Transcoder<T> {

    private static final Log LOG = LogFactory.getLog(AnnotatedBeanTranscoder.class);
    protected SerializationTable serializationTable = new SerializationTable();
    protected Class<T> genericClass;

    /**
     * Create a new transcoder for a given class.
     * @param clazz
     */
    public AnnotatedBeanTranscoder(Class<T> clazz) {
        this.genericClass = clazz;

        if (! genericClass.isAnnotationPresent(SvnStorable.class)) {
		    throw new IllegalArgumentException("Class " + genericClass +
                    " does not conform to SvnStorable annotation.");
		}
    }

    /**
     * Return the serialization table for this transcoder.
     * @return
     */
    public SerializationTable getSerializationTable() {
        return this.serializationTable;
    }

    public T decode(SvnObject svnObject) throws IOException {
        try {
            T obj = genericClass.newInstance();

            for (Method m : genericClass.getMethods()) {
                Class<?> returnType = m.getReturnType();
                if (m.isAnnotationPresent(SvnProperty.class)) {
                    String fieldName = Utils.checkStorability(m, genericClass);
                    String setterName = Utils.getSetMethod(fieldName);
                    Method setMethod = genericClass.getMethod(setterName, returnType);

                    if (serializationTable.getStorageMethod(returnType) == SerializationTable.StoreMethod.SERIALIZE_OBJECT) {
                        SvnPropertyObject propObj = svnObject.getProperty(fieldName);
                        setFieldFromSerializable(setMethod, obj, propObj.getValue());
                    } else {
                        SvnPropertyObject propObj = svnObject.getProperty(fieldName);
                        setFieldFromString(setMethod, returnType, obj, propObj.getValueAsString());
                    }
                }
                if (m.isAnnotationPresent(SvnContent.class)) {
                    String fieldName = Utils.checkStorability(m, genericClass);
                    String setterName = Utils.getSetMethod(fieldName);
                    Method setMethod = genericClass.getMethod(setterName, returnType);
                    if (serializationTable.getStorageMethod(returnType) == SerializationTable.StoreMethod.SERIALIZE_OBJECT) {
                        setFieldFromBytes(setMethod, obj, svnObject.getContent());
                    } else {
                        setFieldFromString(setMethod, returnType, obj, new String(svnObject.getContent()));
                    }
                }
            }

            return obj;
        } catch (Exception e) {
            throw new IOException(e);
        }
    }

    protected void setFieldFromBytes(Method setMethod, Object obj, byte[] value) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(value));
        Object setObj = ois.readObject();
        setMethod.invoke(obj, setObj);
    }

    protected void setFieldFromSerializable(Method setMethod, Object obj, Serializable value) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, IOException, ClassNotFoundException {
        if (value == null) return;
        setMethod.invoke(obj, value);
    }

    protected void setFieldFromString(Method setMethod, Class<?> paramType, Object obj, String value) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, IOException, ClassNotFoundException, SecurityException, NoSuchMethodException, InstantiationException {
        if (value == null) return;

        SerializationTable.StoreMethod storeMethod = serializationTable.getStorageMethod(paramType);

        switch (storeMethod) {
            case TO_STRING:
                if (paramType == String.class) {
                    setMethod.invoke(obj, value);
                } else if (paramType == int.class) {
                    setMethod.invoke(obj, Integer.parseInt(value));
                } else if (paramType == byte.class) {
                    setMethod.invoke(obj, Byte.parseByte(value));
                } else if (paramType == short.class) {
                    setMethod.invoke(obj, Short.parseShort(value));
                } else if (paramType == long.class) {
                    setMethod.invoke(obj, Long.parseLong(value));
                } else if (paramType == float.class) {
                    setMethod.invoke(obj, Float.parseFloat(value));
                } else if (paramType == double.class) {
                    setMethod.invoke(obj, Double.parseDouble(value));
                } else if (paramType == boolean.class) {
                    setMethod.invoke(obj, Boolean.parseBoolean(value));
                } else if (paramType == char.class) {
                    setMethod.invoke(obj, value.charAt(0));
                } else if (paramType == Character.class) {
                    setMethod.invoke(obj, new Character(value.charAt(0)));
                }
                break;
            case TO_STRING_CONSTRUCTOR:
                Constructor<?> c = paramType.getConstructor(String.class);
                Object newInst = c.newInstance(value);
                setMethod.invoke(obj, newInst);
                break;
            case REGISTERED:
                Object deserializedObj = serializationTable.deserializeCustom(value, paramType);
                setMethod.invoke(obj, deserializedObj);
                break;
        }
    }

    public SvnObject encode(Object o) throws IOException {

        Class<?> clazz = o.getClass();
		if (! clazz.isAnnotationPresent(SvnStorable.class)) {
			throw new IllegalArgumentException("Class " + clazz + " does not conform to SvnStorable annotation.");
		}

		SvnStorable s = (SvnStorable) clazz.getAnnotation(SvnStorable.class);
		int classVersion = s.version();
		LOG.info("Class version = " + classVersion);
		SvnObject svnObject = new SvnObject(classVersion + "", "org.restlesscode.javersion.bean.AnnotatedBeanTranscoder");

		for (Method m : clazz.getMethods()) {
			if (m.isAnnotationPresent(SvnProperty.class)) {
				LOG.info("Will store " + m.getName() + " as " + m.getReturnType());
				String fieldName = Utils.checkStorability(m, clazz);
				try {
                    Serializable content = (Serializable) m.invoke(o);
                    if (content != null) {
						svnObject.setProperty(fieldName, new SvnPropertyObject(content, SvnPropertyObject.PropertyType.OBJECT));

                        SerializationTable.StoreMethod storeMethod = serializationTable.getStorageMethod(content.getClass());

                        switch (storeMethod) {
                            case TO_STRING:
                            case TO_STRING_CONSTRUCTOR:
                                svnObject.setProperty(fieldName,
                                        new SvnPropertyObject(content.toString(), SvnPropertyObject.PropertyType.STRING));
                                break;
                            case REGISTERED:
                                String customSerialization = serializationTable.serializeCustom(content);
                                svnObject.setProperty(fieldName,
                                        new SvnPropertyObject(customSerialization, SvnPropertyObject.PropertyType.STRING));
                                break;
                            case SERIALIZE_OBJECT:
                                svnObject.setProperty(fieldName,
                                        new SvnPropertyObject(content, SvnPropertyObject.PropertyType.OBJECT));
                                break;
                        }

                    }
				} catch (Exception e) {
					LOG.error(e);
					throw new IOException(e);
				}
			}
			if (m.isAnnotationPresent(SvnContent.class)) {
				LOG.info("Will store " + m.getName() + " as " + m.getReturnType());
				if (svnObject.getContent() != null) {
					throw new IOException("Cannot mark more than one field as SvnContent");
				}

                try {
                    Serializable content = (Serializable) m.invoke(o);
                    SerializationTable.StoreMethod storeMethod = serializationTable.getStorageMethod(content.getClass());

                    switch (storeMethod) {
                        case TO_STRING:
                        case TO_STRING_CONSTRUCTOR:
                            svnObject.setContent(content.toString().getBytes());
                            break;
                        case REGISTERED:
                            String customSerialization = serializationTable.serializeCustom(content);
                            svnObject.setContent(customSerialization.getBytes());
                            break;
                        case SERIALIZE_OBJECT:
                            ByteArrayOutputStream baos = new ByteArrayOutputStream();
                            ObjectOutputStream oos = new ObjectOutputStream(baos);
                            oos.writeObject(content);
                            oos.flush();
                            svnObject.setContent(baos.toByteArray());
                            break;
                    }
				} catch (Exception e) {
					LOG.error(e);
					throw new IOException(e);
				}
			}
		}

        return svnObject;
    }
}
